﻿// Copyright 2008 Chris Varley (http://www.contractdriven.net/)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Reflection;
using ContractDriven;
using ContractDriven.Exceptions;
using System.Globalization;

namespace ContractDriven
{
	/// <summary>
	/// Provides a set of methods to help enforce programmatic contracts within your code.
	/// </summary>
	public static class Contract
	{

		#region Delegate definitions for ClassInvariant Assertions

		/// <summary>
		/// A method that returns true or false to be used as an invariant.
		/// </summary>
		/// <returns>'true', if the assertion is true, 'false' if the assertion is false.</returns>
		public delegate bool AssertionStatementDelegate();

		/// <summary>
		/// A method that returns a strongly typed BaseAssertion to be used as an invariant.
		/// </summary>
		/// <returns>A BaseAssertion, that, via it's Eval() method returns 'true', if the assertion is true, 'false' if the assertion is false.</returns>
        public delegate IAssertion AssertionObjectDelegate();

		#endregion

		#region Require (Pre-Conditions)

		/// <summary>
		/// Guarantees a pre-condition. Throws a ContractException if the condition evaluates to false.
		/// </summary>
		/// <param name="assertion">A condition that evaluates to true or false.</param>
		public static void Require(bool assertion)
		{
			if(!assertion) throw new PreconditionException("Precondition failed.");
		}

		/// <summary>
		/// Guarantees a pre-condition. Throws a ContractException if the condition evaluates to false.
		/// </summary>
		/// <param name="assertion">A condition that evaluates to true or false.</param>
		/// <param name="description">An exception message to display if false.</param>
		public static void Require(bool assertion, string description)
		{
            if (!assertion) 
                throw new PreconditionException(string.Format(CultureInfo.CurrentCulture, "Precondition failed: {0}", description));
		}


		/// <summary>
		/// Guarantees a pre-condition. Throws a ContractException if the condition evaluates to false.
		/// </summary>
		/// <param name="assertion">A BaseAssertion, that, via it's Eval() method returns 'true', if the assertion is true, 'false' if the assertion is false.</param>
        public static void Require(IAssertion assertion)
		{
			if (!assertion.Eval())
			{
                throw new PreconditionException(string.Format(CultureInfo.CurrentCulture, "Precondition failed : {0}", assertion.FailureDescription));
			}
		}

		#endregion

		#region Ensure (Post-Conditions)
		
		/// <summary>
		/// Guarantees a post-condition. Throws a ContractException if the condition evaluates to false.
		/// </summary>
		/// <param name="assertion">A condition that evaluates to true or false.</param>
		public static void Ensure(bool assertion)
		{
			if(!assertion) throw new PostconditionException("Postcondition failed.");
		}

		/// <summary>
		/// Guarantees a pre-condition. Throws a ContractException if the condition evaluates to false.
		/// </summary>
		/// <param name="assertion">A condition that evaluates to true or false.</param>
		/// <param name="description">An exception message to display if false.</param>
		public static void Ensure(bool assertion, string description)
		{
            if (!assertion) throw new PostconditionException(string.Format(CultureInfo.CurrentCulture, "Postcondition failed: {0}", description));
		}

		/// <summary>
		/// Guarantees a pre-condition. Throws a ContractException if the condition evaluates to false.
		/// </summary>
		/// <param name="assertion">A BaseAssertion, that, via it's Eval() method returns 'true', if the assertion is true, 'false' if the assertion is false.</param>
		public static void Ensure(IAssertion assertion)
		{
			if (!assertion.Eval())
			{
                throw new PostconditionException(string.Format(CultureInfo.CurrentCulture, "Postcondition failed : {0}", assertion.FailureDescription));
			}
		}

		#endregion
	
		#region Invariant (Class Validation)

		/// <summary>
		/// Guarantees an invariant state.
		/// </summary>
		/// <param name="context">The object to which the assertion applies (normally the calling object "this" or "Me" in C#/VB.net respectively.)</param>
		/// <param name="assertion">A delegate for a condition that evaluates to true or false.</param>
		/// <param name="description"></param>
		public static void Invariant(MarshalByRefObject context, AssertionStatementDelegate assertion, string description)
		{
			InvariantCache.Add(context, assertion, description);
			Require(assertion.Invoke(), description);
		}

		/// <summary>
		/// Guarantees an invariant state.
		/// </summary>
		/// <param name="context">The object to which the assertion applies (normally the calling object "this" or "Me" in C#/VB.net respectively.)</param>
		/// <param name="assertion">A delegate for a BaseAssertion, that, via it's Eval() method returns 'true', if the assertion is true, 'false' if the assertion is false.</param>
		public static void Invariant(MarshalByRefObject context, AssertionObjectDelegate assertion)
		{
			InvariantCache.Add(context, assertion);
			Require(assertion.Invoke());
		}
		
		/// <summary>
		/// Assigns 'value' to 'target' and checks invariant state rules for this object. Throws a ContractException if the condition evaluates to false.
		/// </summary>
		/// <typeparam name="T">You can ignore the type paremter in this case due to the dark art of <see cref="http://msdn.microsoft.com/en-us/library/aa691336(VS.71).aspx">Overload Resolution</see>
		/// </typeparam>
		/// <param name="context">The object containing the field to which a value will be assigned.</param>
		/// <param name="target">The field to be assigned to.</param>
		/// <param name="value">The value to be assigned.</param>
		public static void Assign<T>(MarshalByRefObject context, ref T target, T value)
		{
			target = value;
			CheckInvariants(context);
		}

		/// <summary>
		/// Used in CheckInvariants() method to pass empty parameters via DynamicInvoke.
		/// </summary>
		private readonly static object[] _emptyObjArr = new object[] {};

		/// <summary>
		/// Checks all invariant state rules for this object. Throws a ContractException if any of the conditions evaluates to false.
		/// </summary>
		/// <param name="context">The object against which invariant assertions should be run.</param>
		public static void CheckInvariants(MarshalByRefObject context)
		{
			foreach (KeyValuePair<Delegate, string> assertion in InvariantCache.GetAssertions(context))
			{
				try
				{
					if (assertion.Key is AssertionStatementDelegate)
					{
						//check explitly stated assertion
						var passed = (bool) assertion.Key.DynamicInvoke(_emptyObjArr);
						if (!passed)
						{
							throw new InvariantException(string.Format(CultureInfo.CurrentCulture,
							                            	"ClassInvariant check failed for object '{0}' : {1} ",
							                            	context, assertion.Value));
						}
					}
					else if (assertion.Key is AssertionObjectDelegate)
					{
						//check strongly typed assertion
						var current = (IAssertion) assertion.Key.DynamicInvoke(_emptyObjArr);
						if (!current.Eval())
						{
                            throw new InvariantException(string.Format(CultureInfo.CurrentCulture,
							                            	"ClassInvariant check failed for object '{0}' : {1} ",
							                            	context, current.FailureDescription));

						}
					}
				}
				catch(TargetInvocationException tex)
				{
					//we're not actually interested in the fact that the exception was raised 
					//in response to invoking the delgate method at this point, 
					//we are just interested in the fact that the contract has been broken.
					if (tex.InnerException is ContractException)
					{
						throw tex.InnerException;
					}
					throw;
				}

			}
		}

		/// <summary>
		/// Clears an objects invariant assertions from the cache.
		/// You should call this from an objects Dispose() method. (see documentation for details.)
		/// </summary>
		/// <param name="context">The object for which invariants should be cleared.</param>
		public static void Unload(MarshalByRefObject context)
		{
			InvariantCache.Remove(context);
		}
		
		#endregion

		#region Side Effect Checks

		/// <summary>
		/// A cache of values that are currently being tracked in code.
		/// </summary>
		private readonly static Dictionary<Guid, ValueType> _valueTrackingCache = new Dictionary<Guid, ValueType>();

		/// <summary>
		/// Tracks a value for later comparison.
		/// </summary>
		/// <param name="value">The value to track.</param>
		/// <returns>A globally unique identifier (GUID) to retrive the item for comparison.</returns>
		public static Guid Track(ValueType value)
		{
			//add value (or copy of original object) to "before" cache
			var key = Guid.NewGuid();
			_valueTrackingCache.Add(key, value);
			return key;
		}

		/// <summary>
		/// <para>Retrieves a previously tracked value by it's globally unique identifier (GUID)</para>
		/// </summary>
		/// <param name="key">the GUID returned from a call to Track(value)</param>
		/// <returns>The previous value</returns>
		public static ValueType Before(Guid key)
		{
			var returnVal = _valueTrackingCache[key];
			_valueTrackingCache.Remove(key);
			return returnVal;
		}

		#endregion

		#region Application Shutdown Checks

		/// <summary>
		/// You can call this just prior to your application shutdown code to help you ensure 
		/// that all ContractBoundObject classes have been disposed of correctly.
		/// </summary>
		public static void ShutdownCheck()
		{
			if(InvariantCache.RegisteredTypes.Length > 0)
			{
				throw new ContractDrivenFrameworkException(
                    string.Format(CultureInfo.CurrentCulture, Resource.ShutDownCheck,
					InvariantCache.RegisteredTypes.Length,
					string.Join(" ,", InvariantCache.RegisteredTypes)));
			}
		}

		#endregion

	}
}
